﻿/*
  KeePass Password Safe - The Open-Source Password Manager
  Copyright (C) 2003-2021 Dominik Reichl <dominik.reichl@t-online.de>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml.Serialization;

using KeePassLib.Cryptography;
using KeePassLib.Resources;
using KeePassLib.Serialization;
using KeePassLib.Utility;

namespace KeePassLib.Keys
{
  [XmlType("KeyFile")]
  public sealed class KfxFile
  {
    private const ulong KfxVersionCriticalMask = 0xFFFF000000000000UL;
    private const int KfxDataHashLength = 4;

    private KfxMeta m_meta = new KfxMeta();
    public KfxMeta Meta
    {
      get { return m_meta; }
      set
      {
        if (value == null) throw new ArgumentNullException("value");
        m_meta = value;
      }
    }

    private KfxKey m_key = new KfxKey();
    public KfxKey Key
    {
      get { return m_key; }
      set
      {
        if (value == null) throw new ArgumentNullException("value");
        m_key = value;
      }
    }

    public static KfxFile Create(ulong uVersion, byte[] pbKey, byte[] pbHash)
    {
      if (pbKey == null) throw new ArgumentNullException("pbKey");
      if (pbKey.Length == 0) throw new ArgumentOutOfRangeException("pbKey");

      if (uVersion == 0) uVersion = 0x0002000000000000;

      // Null hash: generate one, empty hash: store no hash
      if (pbHash == null) pbHash = HashData(pbKey);
      VerifyHash(pbKey, pbHash);

      KfxFile kf = new KfxFile();

      if (uVersion == 0x0001000000000000)
        kf.Meta.Version = "1.00"; // KeePass <= 2.46 used two zeros
      else kf.Meta.Version = StrUtil.VersionToString(uVersion, 2);

      if (uVersion == 0x0001000000000000)
        kf.Key.Data.Value = Convert.ToBase64String(pbKey);
      else if (uVersion == 0x0002000000000000)
      {
        kf.Key.Data.Value = FormatKeyHex(pbKey, 3);

        if (pbHash.Length != 0)
          kf.Key.Data.Hash = MemUtil.ByteArrayToHexString(pbHash);
      }
      else throw new NotSupportedException(KLRes.FileVersionUnsupported);

      return kf;
    }

    internal static KfxFile Create(ulong uVersion, string strKey, string strHash)
    {
      byte[] pbKey = ParseKey(uVersion, strKey);
      byte[] pbHash = ((strHash != null) ? ParseHash(strHash) : null);

      return Create(uVersion, pbKey, pbHash);
    }

    internal static bool CanLoad(string strFilePath)
    {
      if (string.IsNullOrEmpty(strFilePath)) { Debug.Assert(false); return false; }

      try
      {
        IOConnectionInfo ioc = IOConnectionInfo.FromPath(strFilePath);
        using (Stream s = IOConnection.OpenRead(ioc))
        {
          return (Load(s) != null);
        }
      }
      catch (Exception) { }

      return false;
    }

    public static KfxFile Load(Stream s)
    {
      return XmlUtilEx.Deserialize<KfxFile>(s);
    }

    public void Save(Stream s)
    {
      XmlUtilEx.Serialize<KfxFile>(s, this, true);
    }

    private static string FormatKeyHex(byte[] pb, int cTabs)
    {
      StringBuilder sb = new StringBuilder();
      string str = MemUtil.ByteArrayToHexString(pb);

      for (int i = 0; i < str.Length; ++i)
      {
        if ((i & 0x1F) == 0)
        {
          sb.AppendLine();
          sb.Append('\t', cTabs);
        }
        else if ((i & 0x07) == 0) sb.Append(' ');

        sb.Append(str[i]);
      }

      sb.AppendLine();
      if (cTabs > 0) sb.Append('\t', cTabs - 1);
      return sb.ToString();
    }

    private ulong GetVersion()
    {
      string str = m_meta.Version;
      if (string.IsNullOrEmpty(str)) return 0;

      return StrUtil.ParseVersion(str);
    }

    public byte[] GetKey()
    {
      ulong uVersion = GetVersion();

      byte[] pbKey = ParseKey(uVersion, m_key.Data.Value);
      if ((pbKey == null) || (pbKey.Length == 0))
        throw new FormatException(KLRes.FileCorrupted);

      byte[] pbHash = ParseHash(m_key.Data.Hash);
      VerifyHash(pbKey, pbHash);

      return pbKey;
    }

    private static byte[] HashData(byte[] pb)
    {
      return MemUtil.Mid(CryptoUtil.HashSha256(pb), 0, KfxDataHashLength);
    }

    private static void VerifyHash(byte[] pbKey, byte[] pbHash)
    {
      // The hash is optional; empty hash means success
      if ((pbHash == null) || (pbHash.Length == 0)) return;

      byte[] pbHashCmp = HashData(pbKey);
      if (!MemUtil.ArraysEqual(pbHash, pbHashCmp))
        throw new Exception("Keyfile hash mismatch!");
    }

    private static byte[] ParseKey(ulong uVersion, string strKey)
    {
      if (strKey == null) throw new ArgumentNullException("strKey");

      strKey = StrUtil.RemoveWhiteSpace(strKey);
      if (string.IsNullOrEmpty(strKey)) return MemUtil.EmptyByteArray;

      uVersion &= KfxVersionCriticalMask;

      byte[] pbKey;
      if (uVersion == 0x0001000000000000)
        pbKey = Convert.FromBase64String(strKey);
      else if (uVersion == 0x0002000000000000)
        pbKey = ParseHex(strKey);
      else throw new NotSupportedException(KLRes.FileVersionUnsupported);

      return pbKey;
    }

    private static byte[] ParseHash(string strHash)
    {
      return ParseHex(strHash);
    }

    private static byte[] ParseHex(string str)
    {
      if (str == null) throw new ArgumentNullException("str");
      if (str.Length == 0) return MemUtil.EmptyByteArray;

      if (((str.Length & 1) != 0) || !StrUtil.IsHexString(str, true))
        throw new FormatException();

      return MemUtil.HexStringToByteArray(str);
    }
  }

  public sealed class KfxMeta
  {
    private string m_strVersion = string.Empty;
    [DefaultValue("")]
    public string Version
    {
      get { return m_strVersion; }
      set
      {
        if (value == null) throw new ArgumentNullException("value");
        m_strVersion = value;
      }
    }
  }

  public sealed class KfxKey
  {
    private KfxData m_data = new KfxData();
    public KfxData Data
    {
      get { return m_data; }
      set
      {
        if (value == null) throw new ArgumentNullException("value");
        m_data = value;
      }
    }
  }

  public sealed class KfxData
  {
    private string m_strHash = string.Empty;
    [DefaultValue("")]
    [XmlAttribute("Hash")]
    public string Hash
    {
      get { return m_strHash; }
      set
      {
        if (value == null) throw new ArgumentNullException("value");
        m_strHash = value;
      }
    }

    private string m_strValue = string.Empty;
    [DefaultValue("")]
    [XmlText]
    public string Value
    {
      get { return m_strValue; }
      set
      {
        if (value == null) throw new ArgumentNullException("value");
        m_strValue = value;
      }
    }
  }
}
